import { Box3, BufferAttribute, BufferGeometry, Data3DTexture, FloatType, NearestFilter, Points, RedFormat, ShaderMaterial, TextureFilter, Vector3 } from 'three'
import { CqkjMap } from '../Basic/CqkjMap'
import { DataElement } from '../Basic/DataElement'
import { getValue, OneDimensional } from '../Util/common'
import { PROJECTION } from '../Util/static'
import { Grid2Data } from './Grid2Data'

export interface Grid3DataOption {
  data: Float32Array
  box: Box3
  filter: TextureFilter
  size: Vector3
}

export class Grid3Data extends DataElement<Grid3DataOption> {
  /** 数据贴图 */
  get texture () {
    return getValue(this._cache, 'texture', () => {
      const { data, size, filter } = this.option
      const texture = new Data3DTexture(data, size.x, size.y, size.z)
      texture.format = RedFormat
      texture.type = FloatType
      texture.minFilter = filter
      texture.magFilter = filter
      texture.unpackAlignment = 1
      texture.needsUpdate = true
      return texture
    })
  }

  get xToI () {
    return getValue(this._cache, 'xToI', () => {
      const { box, size } = this.option
      return new OneDimensional([box.min.x, box.max.x], [-0.5, size.x - 0.5])
    })
  }

  get xToU () {
    return getValue(this._cache, 'xToU', () => {
      const { box } = this.option
      return new OneDimensional([box.min.x, box.max.x])
    })
  }

  get yToJ () {
    return getValue(this._cache, 'yToJ', () => {
      const { box, size } = this.option
      return new OneDimensional([box.min.y, box.max.y], [-0.5, size.y - 0.5])
    })
  }

  get yToV () {
    return getValue(this._cache, 'yToV', () => {
      const { box } = this.option
      return new OneDimensional([box.min.y, box.max.y])
    })
  }

  get zToK () {
    return getValue(this._cache, 'zToK', () => {
      const { box, size } = this.option
      return new OneDimensional([box.min.z, box.max.z], [-0.5, size.z - 0.5])
    })
  }

  get zToW () {
    return getValue(this._cache, 'zToW', () => {
      const { box } = this.option
      return new OneDimensional([box.min.z, box.max.z])
    })
  }

  get max () {
    return getValue(this._cache, 'max', () => {
      const { data } = this.option
      let max = data[0]
      for (let i = 1; i < data.length; i++) {
        if (max < data[i]) max = data[i]
      }
      return max
    })
  }

  get min () {
    return getValue(this._cache, 'min', () => {
      const { data } = this.option
      let min = data[0]
      for (let i = 1; i < data.length; i++) {
        if (min > data[i]) min = data[i]
      }
      return min
    })
  }

  debug (map: CqkjMap): void {
    const { texture, min, max, xToI, yToJ, xToU, yToV, zToK, zToW } = this
    const { size } = this.option

    const geometry = new BufferGeometry()
    const position = new Float32Array(size.x * size.y * size.z * 3)
    const uvw = new Float32Array(size.x * size.y * size.z * 3)
    for (let k = 0, _ = 0; k < size.z; k++) {
      const z = zToK.backward(k)
      const w = zToW.forward(z)
      for (let j = 0; j < size.y; j++) {
        const y = yToJ.backward(j)
        const v = yToV.forward(y)
        for (let i = 0; i < size.x; i++, _++) {
          const x = xToI.backward(i)
          const u = xToU.forward(x)
          const _3 = _ * 3

          position[_3] = x
          position[_3 + 1] = y
          position[_3 + 2] = z
          uvw[_3] = u
          uvw[_3 + 1] = v
          uvw[_3 + 2] = w
        }
      }
    }

    geometry.attributes.position = new BufferAttribute(
      position,
      3
    )

    geometry.attributes.uvw = new BufferAttribute(
      uvw,
      3
    )

    const mesh = new Points(geometry, new ShaderMaterial({
      transparent: true,
      depthWrite: false,
      depthTest: false,
      uniforms: {
        u_data: {
          value: texture
        },
        u_min: {
          value: min
        },
        u_max: {
          value: max
        }
      },
      vertexShader: `
      attribute vec3 uvw;
      varying vec3 v_uvw;
      void main(){
          v_uvw=uvw;
          gl_Position=projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
          gl_PointSize=8.0;
      }
      `,
      fragmentShader: `
      precision mediump sampler3D;
      varying vec3 v_uvw;
      uniform sampler3D u_data;
      uniform float u_min;
      uniform float u_max;
      void main() { 
        float v=(texture(u_data,v_uvw).r-u_min)/(u_max-u_min);
        gl_FragColor=mix(vec4(0.0,1.0,0.0,1.0),vec4(1.0,0.0,0.0,1.0),v);    
      } 
      `
    }))
    map.scene.add(mesh)
  }

  /**
   * 通过二维网格创建三维网格
   * @param grids [高度,二维网格][] 要求从低到高排列至少有两个元素
   * @param projection 地图投影
   * @param step 高度间隔，推荐设置为各个高度差的最大公约数
   * @returns 三维网格
   */
  static fromGrid2Datas (grids: Array<[number, Grid2Data]>, step: number) {
    if (grids.length > 1) {
      const data: number[] = []
      const hMin = grids[0][0]
      const hMax = grids[grids.length - 1][0]
      let grid1 = grids[0]
      let grid2 = grids[1]
      let index = 2
      const { filter, box, size } = grid1[1].option
      for (let h = hMin; h <= hMax; h += step) {
        while (h > grid2[0]) {
          grid1 = grid2
          grid2 = grids[index++]
        }
        let k = (h - grid1[0]) / (grid2[0] - grid1[0])
        if (filter === NearestFilter) {
          k = Math.round(k)
        }
        if (k > 0.00000001) {
          grid1[1].option.data.forEach((v, i) => {
            const v1 = grid2[1].option.data[i]
            data.push(v + (v1 - v) * k)
          })
        } else {
          grid1[1].option.data.forEach(v => {
            data.push(v)
          })
        }
      }
      const stepD2 = step / 2
      return new Grid3Data({
        data: new Float32Array(data),
        box: new Box3(new Vector3(box.min.x, box.min.y,PROJECTION.altToZ(hMin - stepD2)), new Vector3(box.max.x, box.max.y, PROJECTION.altToZ(hMax + stepD2))),
        filter,
        size: new Vector3(size.x, size.y, data.length / size.x / size.y)
      })
    }

    throw new Error('网格数不足')
  }
}
